package org.andengine.opengl.font;

import android.content.res.AssetManager;
import android.util.SparseArray;

import org.andengine.entity.text.Text;
import org.andengine.opengl.font.exception.FontException;
import org.andengine.opengl.font.exception.LetterNotFoundException;
import org.andengine.opengl.texture.ITexture;
import org.andengine.opengl.texture.TextureManager;
import org.andengine.opengl.texture.TextureOptions;
import org.andengine.opengl.texture.bitmap.BitmapTexture;
import org.andengine.opengl.texture.bitmap.BitmapTextureFormat;
import org.andengine.util.StreamUtils;
import org.andengine.util.TextUtils;
import org.andengine.util.adt.io.in.AssetInputStreamOpener;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
 * The {@link BitmapFont} class is capable of parsing and loading <code>*.fnt</code> fonts.
 * A {@link BitmapFont}, just like a {@link Font} can be used to create {@link Text} objects.
 * <p/>
 * Use any of these editors to generate <code>*.fnt</code> fonts:
 * <p/>
 * <ul>
 * <li><a href="http://glyphdesigner.71squared.com/">http://glyphdesigner.71squared.com/</a> (Commercial, Mac OS X)</li>
 * <li><a href="http://www.n4te.com/hiero/hiero.jnlp">http://www.n4te.com/hiero/hiero.jnlp</a> (Free, Java)</li>
 * <li><a href="http://slick.cokeandcode.com/demos/hiero.jnlp">http://slick.cokeandcode.com/demos/hiero.jnlp</a> (Free, Java)</li>
 * <li><a href="http://www.angelcode.com/products/bmfont/">http://www.angelcode.com/products/bmfont/</a> (Free, Windows only)</li>
 * </ul>
 * <p/>
 * (c) Zynga 2011
 *
 * @author Nicolas Gramlich <ngramlich@zynga.com>
 * @since 17:54:59 - 01.11.2011
 */
public class BitmapFont implements IFont {
    // ===========================================================
    // Constants
    // ===========================================================

    private static final String TAG_INFO = "info";
    private static final int TAG_INFO_ATTRIBUTECOUNT = 11;

    private static final String TAG_INFO_ATTRIBUTE_FACE = "face";
    private static final String TAG_INFO_ATTRIBUTE_SIZE = "size";
    private static final String TAG_INFO_ATTRIBUTE_BOLD = "bold";
    private static final String TAG_INFO_ATTRIBUTE_ITALIC = "italic";
    private static final String TAG_INFO_ATTRIBUTE_CHARSET = "charset";
    private static final String TAG_INFO_ATTRIBUTE_UNICODE = "unicode";
    private static final String TAG_INFO_ATTRIBUTE_STRETCHHEIGHT = "stretchH";
    private static final String TAG_INFO_ATTRIBUTE_SMOOTH = "smooth";
    private static final String TAG_INFO_ATTRIBUTE_ANTIALIASED = "aa";
    private static final String TAG_INFO_ATTRIBUTE_PADDING = "padding";
    private static final String TAG_INFO_ATTRIBUTE_SPACING = "spacing";

    private static final int TAG_INFO_ATTRIBUTE_FACE_INDEX = 1;
    private static final int TAG_INFO_ATTRIBUTE_SIZE_INDEX = BitmapFont.TAG_INFO_ATTRIBUTE_FACE_INDEX + 1;
    private static final int TAG_INFO_ATTRIBUTE_BOLD_INDEX = BitmapFont.TAG_INFO_ATTRIBUTE_SIZE_INDEX + 1;
    private static final int TAG_INFO_ATTRIBUTE_ITALIC_INDEX = BitmapFont.TAG_INFO_ATTRIBUTE_BOLD_INDEX + 1;
    private static final int TAG_INFO_ATTRIBUTE_CHARSET_INDEX = BitmapFont.TAG_INFO_ATTRIBUTE_ITALIC_INDEX + 1;
    private static final int TAG_INFO_ATTRIBUTE_UNICODE_INDEX = BitmapFont.TAG_INFO_ATTRIBUTE_CHARSET_INDEX + 1;
    private static final int TAG_INFO_ATTRIBUTE_STRETCHHEIGHT_INDEX = BitmapFont.TAG_INFO_ATTRIBUTE_UNICODE_INDEX + 1;
    private static final int TAG_INFO_ATTRIBUTE_SMOOTH_INDEX = BitmapFont.TAG_INFO_ATTRIBUTE_STRETCHHEIGHT_INDEX + 1;
    private static final int TAG_INFO_ATTRIBUTE_ANTIALIASED_INDEX = BitmapFont.TAG_INFO_ATTRIBUTE_SMOOTH_INDEX + 1;
    private static final int TAG_INFO_ATTRIBUTE_PADDING_INDEX = BitmapFont.TAG_INFO_ATTRIBUTE_ANTIALIASED_INDEX + 1;
    private static final int TAG_INFO_ATTRIBUTE_SPACING_INDEX = BitmapFont.TAG_INFO_ATTRIBUTE_PADDING_INDEX + 1;

    private static final String TAG_COMMON = "common";
    private static final int TAG_COMMON_ATTRIBUTECOUNT = 6;

    private static final String TAG_COMMON_ATTRIBUTE_LINEHEIGHT = "lineHeight";
    private static final String TAG_COMMON_ATTRIBUTE_BASE = "base";
    private static final String TAG_COMMON_ATTRIBUTE_SCALEWIDTH = "scaleW";
    private static final String TAG_COMMON_ATTRIBUTE_SCALEHEIGHT = "scaleH";
    private static final String TAG_COMMON_ATTRIBUTE_PAGES = "pages";
    private static final String TAG_COMMON_ATTRIBUTE_PACKED = "packed";

    private static final int TAG_COMMON_ATTRIBUTE_LINEHEIGHT_INDEX = 1;
    private static final int TAG_COMMON_ATTRIBUTE_BASE_INDEX = BitmapFont.TAG_COMMON_ATTRIBUTE_LINEHEIGHT_INDEX + 1;
    private static final int TAG_COMMON_ATTRIBUTE_SCALEWIDTH_INDEX = BitmapFont.TAG_COMMON_ATTRIBUTE_BASE_INDEX + 1;
    private static final int TAG_COMMON_ATTRIBUTE_SCALEHEIGHT_INDEX = BitmapFont.TAG_COMMON_ATTRIBUTE_SCALEWIDTH_INDEX + 1;
    private static final int TAG_COMMON_ATTRIBUTE_PAGES_INDEX = BitmapFont.TAG_COMMON_ATTRIBUTE_SCALEHEIGHT_INDEX + 1;
    private static final int TAG_COMMON_ATTRIBUTE_PACKED_INDEX = BitmapFont.TAG_COMMON_ATTRIBUTE_PAGES_INDEX + 1;

    private static final String TAG_PAGE = "page";
    private static final int TAG_PAGE_ATTRIBUTECOUNT = 2;

    private static final String TAG_PAGE_ATTRIBUTE_ID = "id";
    private static final String TAG_PAGE_ATTRIBUTE_FILE = "file";

    private static final int TAG_PAGE_ATTRIBUTE_ID_INDEX = 1;
    private static final int TAG_PAGE_ATTRIBUTE_FILE_INDEX = BitmapFont.TAG_PAGE_ATTRIBUTE_ID_INDEX + 1;

    private static final String TAG_CHARS = "chars";
    private static final int TAG_CHARS_ATTRIBUTECOUNT = 1;

    private static final String TAG_CHARS_ATTRIBUTE_COUNT = "count";

    private static final int TAG_CHARS_ATTRIBUTE_COUNT_INDEX = 1;

    private static final String TAG_CHAR = "char";
    private static final int TAG_CHAR_ATTRIBUTECOUNT = 10;

    private static final String TAG_CHAR_ATTRIBUTE_ID = "id";
    private static final String TAG_CHAR_ATTRIBUTE_X = "x";
    private static final String TAG_CHAR_ATTRIBUTE_Y = "y";
    private static final String TAG_CHAR_ATTRIBUTE_WIDTH = "width";
    private static final String TAG_CHAR_ATTRIBUTE_HEIGHT = "height";
    private static final String TAG_CHAR_ATTRIBUTE_XOFFSET = "xoffset";
    private static final String TAG_CHAR_ATTRIBUTE_YOFFSET = "yoffset";
    private static final String TAG_CHAR_ATTRIBUTE_XADVANCE = "xadvance";
    private static final String TAG_CHAR_ATTRIBUTE_PAGE = "page";
//	private static final String TAG_CHAR_ATTRIBUTE_CHANNEL = "chnl";

    private static final int TAG_CHAR_ATTRIBUTE_ID_INDEX = 1;
    private static final int TAG_CHAR_ATTRIBUTE_X_INDEX = BitmapFont.TAG_CHAR_ATTRIBUTE_ID_INDEX + 1;
    private static final int TAG_CHAR_ATTRIBUTE_Y_INDEX = BitmapFont.TAG_CHAR_ATTRIBUTE_X_INDEX + 1;
    private static final int TAG_CHAR_ATTRIBUTE_WIDTH_INDEX = BitmapFont.TAG_CHAR_ATTRIBUTE_Y_INDEX + 1;
    private static final int TAG_CHAR_ATTRIBUTE_HEIGHT_INDEX = BitmapFont.TAG_CHAR_ATTRIBUTE_WIDTH_INDEX + 1;
    private static final int TAG_CHAR_ATTRIBUTE_XOFFSET_INDEX = BitmapFont.TAG_CHAR_ATTRIBUTE_HEIGHT_INDEX + 1;
    private static final int TAG_CHAR_ATTRIBUTE_YOFFSET_INDEX = BitmapFont.TAG_CHAR_ATTRIBUTE_XOFFSET_INDEX + 1;
    private static final int TAG_CHAR_ATTRIBUTE_XADVANCE_INDEX = BitmapFont.TAG_CHAR_ATTRIBUTE_YOFFSET_INDEX + 1;
    private static final int TAG_CHAR_ATTRIBUTE_PAGE_INDEX = BitmapFont.TAG_CHAR_ATTRIBUTE_XADVANCE_INDEX + 1;
//	private static final int TAG_CHAR_ATTRIBUTE_CHANNEL_INDEX = BitmapFont.TAG_CHAR_ATTRIBUTE_PAGE_INDEX + 1;

    private static final String TAG_KERNINGS = "kernings";
    private static final int TAG_KERNINGS_ATTRIBUTECOUNT = 1;

    private static final String TAG_KERNINGS_ATTRIBUTE_COUNT = "count";

    private static final int TAG_KERNINGS_ATTRIBUTE_COUNT_INDEX = 1;

    private static final String TAG_KERNING = "kerning";
    private static final int TAG_KERNING_ATTRIBUTECOUNT = 3;

    private static final String TAG_KERNING_ATTRIBUTE_FIRST = "first";
    private static final String TAG_KERNING_ATTRIBUTE_SECOND = "second";
    private static final String TAG_KERNING_ATTRIBUTE_AMOUNT = "amount";

    private static final int TAG_KERNING_ATTRIBUTE_FIRST_INDEX = 1;
    private static final int TAG_KERNING_ATTRIBUTE_SECOND_INDEX = BitmapFont.TAG_KERNING_ATTRIBUTE_FIRST_INDEX + 1;
    private static final int TAG_KERNING_ATTRIBUTE_AMOUNT_INDEX = BitmapFont.TAG_KERNING_ATTRIBUTE_SECOND_INDEX + 1;

    // ===========================================================
    // Fields
    // ===========================================================

    private final TextureManager mTextureManager;

    private final BitmapTextureFormat mBitmapTextureFormat;
    private final TextureOptions mTextureOptions;

    private final SparseArray<Letter> mCharacterToLetterMap = new SparseArray<Letter>();

    private final BitmapFontInfo mBitmapFontInfo;
    private final BitmapFontPage[] mBitmapFontPages;

    private final int mLineHeight;
    private final int mBase;
    private final int mScaleWidth;
    private final int mScaleHeight;
    private final int mBitmapFontPageCount;
    private final boolean mPacked;
    private final BitmapFontOptions mBitmapFontOptions;

    // ===========================================================
    // Constructors
    // ===========================================================

    public BitmapFont(final TextureManager pTextureManager, final AssetManager pAssetManager, final String pAssetPath) {
        this(pTextureManager, pAssetManager, pAssetPath, BitmapTextureFormat.RGBA_8888, TextureOptions.DEFAULT, BitmapFontOptions.DEFAULT);
    }

    public BitmapFont(final TextureManager pTextureManager, final AssetManager pAssetManager, final String pAssetPath, final BitmapTextureFormat pBitmapTextureFormat) {
        this(pTextureManager, pAssetManager, pAssetPath, pBitmapTextureFormat, TextureOptions.DEFAULT, BitmapFontOptions.DEFAULT);
    }

    public BitmapFont(final TextureManager pTextureManager, final AssetManager pAssetManager, final String pAssetPath, final TextureOptions pTextureOptions) {
        this(pTextureManager, pAssetManager, pAssetPath, BitmapTextureFormat.RGBA_8888, pTextureOptions, BitmapFontOptions.DEFAULT);
    }

    public BitmapFont(final TextureManager pTextureManager, final AssetManager pAssetManager, final String pAssetPath, final BitmapTextureFormat pBitmapTextureFormat, final TextureOptions pTextureOptions) {
        this(pTextureManager, pAssetManager, pAssetPath, pBitmapTextureFormat, pTextureOptions, BitmapFontOptions.DEFAULT);
    }

    public BitmapFont(final TextureManager pTextureManager, final AssetManager pAssetManager, final String pAssetPath, final BitmapTextureFormat pBitmapTextureFormat, final TextureOptions pTextureOptions, final BitmapFontOptions pBitmapFontOptions) {
        this.mTextureManager = pTextureManager;
        this.mBitmapTextureFormat = pBitmapTextureFormat;
        this.mTextureOptions = pTextureOptions;
        this.mBitmapFontOptions = pBitmapFontOptions;

        InputStream in = null;
        try {
            in = pAssetManager.open(pAssetPath);
            final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in), StreamUtils.IO_BUFFER_SIZE);

            final String assetBasePath;
            if (pAssetPath.indexOf('/') == -1) {
                assetBasePath = "";
            } else {
                assetBasePath = pAssetPath.substring(0, pAssetPath.lastIndexOf('/') + 1);
            }

			/* Info. */
            {
                this.mBitmapFontInfo = new BitmapFontInfo(bufferedReader.readLine());
            }

			/* Common. */
            {
                final String common = bufferedReader.readLine();
                if ((common != null) && common.startsWith(BitmapFont.TAG_COMMON)) {
                    final String[] commonAttributes = TextUtils.SPLITPATTERN_SPACE.split(common, BitmapFont.TAG_COMMON_ATTRIBUTECOUNT + 1);
                    if ((commonAttributes.length - 1) != BitmapFont.TAG_COMMON_ATTRIBUTECOUNT) {
                        throw new FontException("Expected: '" + BitmapFont.TAG_COMMON_ATTRIBUTECOUNT + "' " + BitmapFont.TAG_COMMON + " attributes, found: '" + (commonAttributes.length - 1) + "'.");
                    }
                    if (!commonAttributes[0].equals(BitmapFont.TAG_COMMON)) {
                        throw new FontException("Expected: '" + BitmapFont.TAG_COMMON + "' attributes.");
                    }
                    this.mLineHeight = BitmapFont.getIntAttribute(commonAttributes, BitmapFont.TAG_COMMON_ATTRIBUTE_LINEHEIGHT_INDEX, BitmapFont.TAG_COMMON_ATTRIBUTE_LINEHEIGHT);
                    this.mBase = BitmapFont.getIntAttribute(commonAttributes, BitmapFont.TAG_COMMON_ATTRIBUTE_BASE_INDEX, BitmapFont.TAG_COMMON_ATTRIBUTE_BASE);
                    this.mScaleWidth = BitmapFont.getIntAttribute(commonAttributes, BitmapFont.TAG_COMMON_ATTRIBUTE_SCALEWIDTH_INDEX, BitmapFont.TAG_COMMON_ATTRIBUTE_SCALEWIDTH);
                    this.mScaleHeight = BitmapFont.getIntAttribute(commonAttributes, BitmapFont.TAG_COMMON_ATTRIBUTE_SCALEHEIGHT_INDEX, BitmapFont.TAG_COMMON_ATTRIBUTE_SCALEHEIGHT);
                    this.mBitmapFontPageCount = BitmapFont.getIntAttribute(commonAttributes, BitmapFont.TAG_COMMON_ATTRIBUTE_PAGES_INDEX, BitmapFont.TAG_COMMON_ATTRIBUTE_PAGES);
                    this.mPacked = BitmapFont.getBooleanAttribute(commonAttributes, BitmapFont.TAG_COMMON_ATTRIBUTE_PACKED_INDEX, BitmapFont.TAG_COMMON_ATTRIBUTE_PACKED);

                    if (this.mBitmapFontPageCount != 1) {
                        throw new FontException("Only a single page is supported.");
                    }
                    this.mBitmapFontPages = new BitmapFontPage[this.mBitmapFontPageCount];

                    if (this.mPacked) {
                        throw new FontException("Packed is not supported.");
                    }
                } else {
                    throw new FontException("Expected: '" + BitmapFont.TAG_COMMON + "' attributes.");
                }
            }

			/* Pages. */
            for (int i = 0; i < this.mBitmapFontPageCount; i++) {
                this.mBitmapFontPages[i] = new BitmapFontPage(pAssetManager, assetBasePath, bufferedReader.readLine());
            }

			/* Chars. */
            {
                final String chars = bufferedReader.readLine();
                if ((chars != null) && chars.startsWith(BitmapFont.TAG_CHARS)) {
                    final String[] charsAttributes = TextUtils.SPLITPATTERN_SPACE.split(chars, BitmapFont.TAG_CHARS_ATTRIBUTECOUNT + 1);
                    if ((charsAttributes.length - 1) != BitmapFont.TAG_CHARS_ATTRIBUTECOUNT) {
                        throw new FontException("Expected: '" + BitmapFont.TAG_CHARS_ATTRIBUTECOUNT + "' " + BitmapFont.TAG_CHARS + " attributes, found: '" + (charsAttributes.length - 1) + "'.");
                    }
                    if (!charsAttributes[0].equals(BitmapFont.TAG_CHARS)) {
                        throw new FontException("Expected: '" + BitmapFont.TAG_CHARS + "' attributes.");
                    }

                    final int characterCount = BitmapFont.getIntAttribute(charsAttributes, BitmapFont.TAG_CHARS_ATTRIBUTE_COUNT_INDEX, BitmapFont.TAG_CHARS_ATTRIBUTE_COUNT);

                    this.parseCharacters(characterCount, bufferedReader);
                } else {
                    throw new FontException("Expected: '" + BitmapFont.TAG_CHARS + "' attributes.");
                }
            }

			/* Kernings. */
            {
                final String kernings = bufferedReader.readLine();
                if ((kernings != null) && kernings.startsWith(BitmapFont.TAG_KERNINGS)) {
                    final String[] kerningsAttributes = TextUtils.SPLITPATTERN_SPACE.split(kernings, BitmapFont.TAG_KERNINGS_ATTRIBUTECOUNT + 1);
                    if ((kerningsAttributes.length - 1) != BitmapFont.TAG_KERNINGS_ATTRIBUTECOUNT) {
                        throw new FontException("Expected: '" + BitmapFont.TAG_KERNINGS_ATTRIBUTECOUNT + "' " + BitmapFont.TAG_KERNINGS + " attributes, found: '" + (kerningsAttributes.length - 1) + "'.");
                    }
                    if (!kerningsAttributes[0].equals(BitmapFont.TAG_KERNINGS)) {
                        throw new FontException("Expected: '" + BitmapFont.TAG_KERNINGS + "' attributes.");
                    }

                    final int kerningsCount = BitmapFont.getIntAttribute(kerningsAttributes, BitmapFont.TAG_KERNINGS_ATTRIBUTE_COUNT_INDEX, BitmapFont.TAG_KERNINGS_ATTRIBUTE_COUNT);

                    this.parseKernings(kerningsCount, bufferedReader);
                }
            }
        } catch (final IOException e) {
            throw new FontException("Failed loading BitmapFont. AssetPath: " + pAssetPath, e);
        } finally {
            StreamUtils.close(in);
        }
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    public BitmapFontInfo getBitmapFontInfo() {
        return this.mBitmapFontInfo;
    }

    public int getBase() {
        return this.mBase;
    }

    public int getScaleWidth() {
        return this.mScaleWidth;
    }

    public int getScaleHeight() {
        return this.mScaleHeight;
    }

    public int getBitmapFontPageCount() {
        return this.mBitmapFontPageCount;
    }

    public BitmapFontPage[] getBitmapFontPages() {
        return this.mBitmapFontPages;
    }

    public BitmapFontPage getBitmapFontPage(final int pIndex) {
        return this.mBitmapFontPages[pIndex];
    }

    public boolean isPacked() {
        return this.mPacked;
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    @Override
    public ITexture getTexture() {
        return this.mBitmapFontPages[0].getTexture();
    }

    @Override
    public void load() {
        this.loadTextures();
    }

    @Override
    public void unload() {
        this.unloadTextures();
    }

    @Override
    public float getLineHeight() {
        return this.mLineHeight;
    }

    @Override
    public Letter getLetter(final char pChar) throws LetterNotFoundException {
        final Letter letter = this.mCharacterToLetterMap.get(pChar);
        if (letter == null) {
            throw new LetterNotFoundException("Letter '" + pChar + "' not found.");
        }
        return letter;
    }

    // ===========================================================
    // Methods
    // ===========================================================

    public void loadTextures() {
        final BitmapFontPage[] bitmapFontPages = this.mBitmapFontPages;
        final int bitmapFontPageCount = bitmapFontPages.length;
        for (int i = 0; i < bitmapFontPageCount; i++) {
            bitmapFontPages[i].getTexture().load();
        }
    }

    public void unloadTextures() {
        final BitmapFontPage[] bitmapFontPages = this.mBitmapFontPages;
        final int bitmapFontPageCount = bitmapFontPages.length;
        for (int i = 0; i < bitmapFontPageCount; i++) {
            bitmapFontPages[i].getTexture().unload();
        }
    }

    private void parseCharacters(final int pCharacterCount, final BufferedReader pBufferedReader) throws IOException {
        for (int i = pCharacterCount - 1; i >= 0; i--) {
            final String character = pBufferedReader.readLine();
            final String[] charAttributes = TextUtils.SPLITPATTERN_SPACES.split(character, BitmapFont.TAG_CHAR_ATTRIBUTECOUNT + 1);
            if ((charAttributes.length - 1) != BitmapFont.TAG_CHAR_ATTRIBUTECOUNT) {
                throw new FontException("Expected: '" + BitmapFont.TAG_CHAR_ATTRIBUTECOUNT + "' " + BitmapFont.TAG_CHAR + " attributes, found: '" + (charAttributes.length - 1) + "'.");
            }
            if (!charAttributes[0].equals(BitmapFont.TAG_CHAR)) {
                throw new FontException("Expected: '" + BitmapFont.TAG_CHAR + "' attributes.");
            }

            final char id = BitmapFont.getCharAttribute(charAttributes, BitmapFont.TAG_CHAR_ATTRIBUTE_ID_INDEX, BitmapFont.TAG_CHAR_ATTRIBUTE_ID);
            final int x = this.mBitmapFontOptions.mTextureOffsetX + BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_CHAR_ATTRIBUTE_X_INDEX, BitmapFont.TAG_CHAR_ATTRIBUTE_X);
            final int y = this.mBitmapFontOptions.mTextureOffsetY + BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_CHAR_ATTRIBUTE_Y_INDEX, BitmapFont.TAG_CHAR_ATTRIBUTE_Y);
            final int width = BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_CHAR_ATTRIBUTE_WIDTH_INDEX, BitmapFont.TAG_CHAR_ATTRIBUTE_WIDTH);
            final int height = BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_CHAR_ATTRIBUTE_HEIGHT_INDEX, BitmapFont.TAG_CHAR_ATTRIBUTE_HEIGHT);
            final int xOffset = BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_CHAR_ATTRIBUTE_XOFFSET_INDEX, BitmapFont.TAG_CHAR_ATTRIBUTE_XOFFSET);
            final int yOffset = BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_CHAR_ATTRIBUTE_YOFFSET_INDEX, BitmapFont.TAG_CHAR_ATTRIBUTE_YOFFSET);
            final int xAdvance = BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_CHAR_ATTRIBUTE_XADVANCE_INDEX, BitmapFont.TAG_CHAR_ATTRIBUTE_XADVANCE);
            final int page = BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_CHAR_ATTRIBUTE_PAGE_INDEX, BitmapFont.TAG_CHAR_ATTRIBUTE_PAGE);
//			final int channel = BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_CHAR_ATTRIBUTE_CHANNEL_INDEX, BitmapFont.TAG_CHAR_ATTRIBUTE_CHANNEL);

            final ITexture bitmapFontPageTexture = this.mBitmapFontPages[page].getTexture();
            final float textureWidth = bitmapFontPageTexture.getWidth();
            final float textureHeight = bitmapFontPageTexture.getHeight();

            final float u = x / textureWidth;
            final float v = y / textureHeight;
            final float u2 = (x + width) / textureWidth;
            final float v2 = (y + height) / textureHeight;

            this.mCharacterToLetterMap.put(id, new Letter(id, x, y, width, height, xOffset, yOffset, xAdvance, u, v, u2, v2));
        }
    }

    private void parseKernings(final int pKerningsCount, final BufferedReader pBufferedReader) throws IOException {
        for (int i = pKerningsCount - 1; i >= 0; i--) {
            final String kerning = pBufferedReader.readLine();
            final String[] charAttributes = TextUtils.SPLITPATTERN_SPACES.split(kerning, BitmapFont.TAG_KERNING_ATTRIBUTECOUNT + 1);
            if ((charAttributes.length - 1) != BitmapFont.TAG_KERNING_ATTRIBUTECOUNT) {
                throw new FontException("Expected: '" + BitmapFont.TAG_KERNING_ATTRIBUTECOUNT + "' " + BitmapFont.TAG_KERNING + " attributes, found: '" + (charAttributes.length - 1) + "'.");
            }
            if (!charAttributes[0].equals(BitmapFont.TAG_KERNING)) {
                throw new FontException("Expected: '" + BitmapFont.TAG_KERNING + "' attributes.");
            }

            final int first = BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_KERNING_ATTRIBUTE_FIRST_INDEX, BitmapFont.TAG_KERNING_ATTRIBUTE_FIRST);
            final int second = BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_KERNING_ATTRIBUTE_SECOND_INDEX, BitmapFont.TAG_KERNING_ATTRIBUTE_SECOND);
            final int amount = BitmapFont.getIntAttribute(charAttributes, BitmapFont.TAG_KERNING_ATTRIBUTE_AMOUNT_INDEX, BitmapFont.TAG_KERNING_ATTRIBUTE_AMOUNT);

            this.mCharacterToLetterMap.get(first).addKerning(second, amount);
        }
    }

    private static boolean getBooleanAttribute(final String[] pData, final int pPosition, final String pAttribute) {
        final String data = pData[pPosition];
        final int attributeLength = pAttribute.length();

        if (!data.startsWith(pAttribute) || (data.charAt(attributeLength) != '=')) {
            throw new FontException("Expected '" + pAttribute + "' at position '" + pPosition + "', but found: '" + data + "'.");
        }

        return Integer.parseInt(data.substring(attributeLength + 1)) != 0;
    }

    private static char getCharAttribute(final String[] pData, final int pPosition, final String pAttribute) {
        return (char) BitmapFont.getIntAttribute(pData, pPosition, pAttribute);
    }

    private static int getIntAttribute(final String[] pData, final int pPosition, final String pAttribute) {
        final String data = pData[pPosition];
        final int attributeLength = pAttribute.length();

        if (!data.startsWith(pAttribute) || (data.charAt(attributeLength) != '=')) {
            throw new FontException("Expected '" + pAttribute + "' at position '" + pPosition + "', but found: '" + data + "'.");
        }

        return Integer.parseInt(data.substring(attributeLength + 1));
    }

    private static String getStringAttribute(final String[] pData, final int pPosition, final String pAttribute) {
        final String data = pData[pPosition];
        final int attributeLength = pAttribute.length();

        if (!data.startsWith(pAttribute) || (data.charAt(attributeLength) != '=')) {
            throw new FontException("Expected '" + pAttribute + "' at position '" + pPosition + "', but found: '" + data + "'.");
        }

        return data.substring(attributeLength + 2, data.length() - 1);
    }

    private static String getAttribute(final String[] pData, final int pPosition, final String pAttribute) {
        final String data = pData[pPosition];
        final int attributeLength = pAttribute.length();

        if (!data.startsWith(pAttribute)) {
            throw new FontException("Expected '" + pAttribute + "' at position '" + pPosition + "', but found: '" + data + "'.");
        }

        return data.substring(attributeLength + 1);
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    public static class BitmapFontOptions {
        // ===========================================================
        // Constants
        // ===========================================================

        public static final BitmapFontOptions DEFAULT = new BitmapFontOptions(0, 0);

        // ===========================================================
        // Fields
        // ===========================================================

        private final int mTextureOffsetX;
        private final int mTextureOffsetY;

        // ===========================================================
        // Constructors
        // ===========================================================

        public BitmapFontOptions(final int pTextureOffsetX, final int pTextureOffsetY) {
            this.mTextureOffsetX = pTextureOffsetX;
            this.mTextureOffsetY = pTextureOffsetY;
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        public int getTextureOffsetX() {
            return this.mTextureOffsetX;
        }

        public int getTextureOffsetY() {
            return this.mTextureOffsetY;
        }

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        // ===========================================================
        // Methods
        // ===========================================================

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }

    public class BitmapFontInfo {
        // ===========================================================
        // Constants
        // ===========================================================

        private static final int PADDING_LEFT_INDEX = 0;
        private static final int PADDING_TOP_INDEX = BitmapFontInfo.PADDING_LEFT_INDEX + 1;
        private static final int PADDING_RIGHT_INDEX = BitmapFontInfo.PADDING_TOP_INDEX + 1;
        private static final int PADDING_BOTTOM_INDEX = BitmapFontInfo.PADDING_RIGHT_INDEX + 1;

        private static final int SPACING_X_INDEX = 0;
        private static final int SPACING_Y_INDEX = BitmapFontInfo.SPACING_X_INDEX + 1;

        // ===========================================================
        // Fields
        // ===========================================================

        private final String mFace;
        private final int mSize;
        private final boolean mBold;
        private final boolean mItalic;
        private final String mCharset;
        private final int mUnicode;
        private final int mStretchHeight;
        private final boolean mSmooth;
        private final boolean mAntiAliased;

        private final int mPaddingLeft;
        private final int mPaddingTop;
        private final int mPaddingRight;
        private final int mPaddingBottom;

        private final int mSpacingX;
        private final int mSpacingY;

        // ===========================================================
        // Constructors
        // ===========================================================

        public BitmapFontInfo(final String pData) throws FontException {
            if (pData == null) {
                throw new FontException("pData must not be null.");
            }

            final String[] infoAttributes = TextUtils.SPLITPATTERN_SPACE.split(pData, BitmapFont.TAG_INFO_ATTRIBUTECOUNT + 1);

            if ((infoAttributes.length - 1) != BitmapFont.TAG_INFO_ATTRIBUTECOUNT) {
                throw new FontException("Expected: '" + BitmapFont.TAG_INFO_ATTRIBUTECOUNT + "' " + BitmapFont.TAG_INFO + " attributes, found: '" + (infoAttributes.length - 1) + "'.");
            }
            if (!infoAttributes[0].equals(BitmapFont.TAG_INFO)) {
                throw new FontException("Expected: '" + BitmapFont.TAG_INFO + "' attributes.");
            }

            this.mFace = BitmapFont.getStringAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_FACE_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_FACE);
            this.mSize = BitmapFont.getIntAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_SIZE_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_SIZE);
            this.mBold = BitmapFont.getBooleanAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_BOLD_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_BOLD);
            this.mItalic = BitmapFont.getBooleanAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_ITALIC_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_ITALIC);
            this.mCharset = BitmapFont.getStringAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_CHARSET_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_CHARSET);
            this.mUnicode = BitmapFont.getIntAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_UNICODE_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_UNICODE);
            this.mStretchHeight = BitmapFont.getIntAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_STRETCHHEIGHT_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_STRETCHHEIGHT);
            this.mSmooth = BitmapFont.getBooleanAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_SMOOTH_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_SMOOTH);
            this.mAntiAliased = BitmapFont.getBooleanAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_ANTIALIASED_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_ANTIALIASED);

            final String padding = BitmapFont.getAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_PADDING_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_PADDING);
            final String[] paddings = TextUtils.SPLITPATTERN_COMMA.split(padding, 4);
            this.mPaddingLeft = Integer.parseInt(paddings[BitmapFontInfo.PADDING_LEFT_INDEX]);
            this.mPaddingTop = Integer.parseInt(paddings[BitmapFontInfo.PADDING_TOP_INDEX]);
            this.mPaddingRight = Integer.parseInt(paddings[BitmapFontInfo.PADDING_RIGHT_INDEX]);
            this.mPaddingBottom = Integer.parseInt(paddings[BitmapFontInfo.PADDING_BOTTOM_INDEX]);

            final String spacing = BitmapFont.getAttribute(infoAttributes, BitmapFont.TAG_INFO_ATTRIBUTE_SPACING_INDEX, BitmapFont.TAG_INFO_ATTRIBUTE_SPACING);
            final String[] spacings = TextUtils.SPLITPATTERN_COMMA.split(spacing, 2);
            this.mSpacingX = Integer.parseInt(spacings[BitmapFontInfo.SPACING_X_INDEX]);
            this.mSpacingY = Integer.parseInt(spacings[BitmapFontInfo.SPACING_Y_INDEX]);
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        public String getFace() {
            return this.mFace;
        }

        public int getSize() {
            return this.mSize;
        }

        public boolean isBold() {
            return this.mBold;
        }

        public boolean isItalic() {
            return this.mItalic;
        }

        public String getCharset() {
            return this.mCharset;
        }

        public int getUnicode() {
            return this.mUnicode;
        }

        public int getStretchHeight() {
            return this.mStretchHeight;
        }

        public boolean isSmooth() {
            return this.mSmooth;
        }

        public boolean isAntiAliased() {
            return this.mAntiAliased;
        }

        public int getPaddingLeft() {
            return this.mPaddingLeft;
        }

        public int getPaddingTop() {
            return this.mPaddingTop;
        }

        public int getPaddingRight() {
            return this.mPaddingRight;
        }

        public int getPaddingBottom() {
            return this.mPaddingBottom;
        }

        public int getSpacingX() {
            return this.mSpacingX;
        }

        public int getSpacingY() {
            return this.mSpacingY;
        }

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        // ===========================================================
        // Methods
        // ===========================================================

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }

    public class BitmapFontPage {
        // ===========================================================
        // Constants
        // ===========================================================

        // ===========================================================
        // Fields
        // ===========================================================

        private int mID;
        private final ITexture mTexture;

        // ===========================================================
        // Constructors
        // ===========================================================

        public BitmapFontPage(final AssetManager pAssetManager, final String pAssetBasePath, final String pData) throws IOException {
            final String[] pageAttributes = TextUtils.SPLITPATTERN_SPACE.split(pData, BitmapFont.TAG_PAGE_ATTRIBUTECOUNT + 1);

            if ((pageAttributes.length - 1) != BitmapFont.TAG_PAGE_ATTRIBUTECOUNT) {
                throw new FontException("Expected: '" + BitmapFont.TAG_PAGE_ATTRIBUTECOUNT + "' " + BitmapFont.TAG_PAGE + " attributes, found: '" + (pageAttributes.length - 1) + "'.");
            }
            if (!pageAttributes[0].equals(BitmapFont.TAG_PAGE)) {
                throw new FontException("Expected: '" + BitmapFont.TAG_PAGE + "' attributes.");
            }

            this.mID = BitmapFont.getIntAttribute(pageAttributes, BitmapFont.TAG_PAGE_ATTRIBUTE_ID_INDEX, BitmapFont.TAG_PAGE_ATTRIBUTE_ID);
            final String file = BitmapFont.getStringAttribute(pageAttributes, BitmapFont.TAG_PAGE_ATTRIBUTE_FILE_INDEX, BitmapFont.TAG_PAGE_ATTRIBUTE_FILE);

            final String assetPath = pAssetBasePath + file;
            this.mTexture = new BitmapTexture(BitmapFont.this.mTextureManager, new AssetInputStreamOpener(pAssetManager, assetPath), BitmapFont.this.mBitmapTextureFormat, BitmapFont.this.mTextureOptions);
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        public int getID() {
            return this.mID;
        }

        public ITexture getTexture() {
            return this.mTexture;
        }

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        // ===========================================================
        // Methods
        // ===========================================================

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }
}
